package com.cache2.intercepter; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.List; import java.util.Set; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.cache2.annotation.Cache2Element; import com.cache2.annotation.CachedMethod; import com.cache2.domain.CacheCommand; import com.cache2.domain.CacheStrategy; import com.cache2.domain.CachedValue; import com.cache2.domain.Identifiable; import com.cache2.helper.Cache1Helper; import com.cache2.helper.Cache2Helper; import com.cache2.key.Cache1Key; import com.cache2.key.Cache2Key; import com.cache2.util.CacheUtil; /** * Intercepts methods marked with {@link CachedMethod} and caches them according * to the diagram. * * <p> * Primary cache (cache1):<br> * ( ) => O * </p> * * <p> * Secondary cache (cache2):<br> * E => ( ) * </p> * * <p> * Key:<br> * ( ) denotes a method signature<br> * O denotes an object<br> * E denotes an element signature (class and id) * </p> * * <p> * Methods marked with {@link CachedMethod} and their returned objects get put * into cache1 using {@link Cache1Key} as the key. When a method gets cached, * the returned object and its arguments are evaluated for * {@link Cached2Element} annotations and if present creates entries in cache2 * using {@link Cache2Key} as a key, and {@link Cache1Key} as a value. This * creates a link between the elements and the methods, which lets us invalidate * the cached methods when we intercept updates on the elements. * </p> * * <p> * Support the following situations: * <ol> * <li>1. A method gets cached and changes to the returned object invalidate it. * This is handled by invalidations on update or delete to the returned object, * as we create a cache2 relating that element back to the method.</li> * <li>2. A method gets cached and the returned entity contains a list of child * entities. If that list is added to or removed from we invalidate the method * cache. Basically the child elements need to invalidate methods linked to the * parent element. This is handled by invalidations on the child element update * or delete, as they normally contain a field of the parent element or its id. * As long as that field is annotated, it will find the method to invalidate. * <li> * </ol> * </p> * * @author matthew * */ @Component @Aspect public class Cache2Intercepter { @Autowired private Cache1Helper cache1Helper; @Autowired private Cache2Helper cache2Helper; /** * Command for putting elements into the cache. */ private final CacheCommand putCommand = new CacheCommand() { @Override public void execute(Cache2Key cache2Key, Cache1Key cache1Key) { cache2Helper.put(cache2Key, cache1Key); } }; /** * Command for invalidating elements from the cache. */ private final CacheCommand invalidateCommand = new CacheCommand() { @Override public void execute(Cache2Key cache2Key, Cache1Key cache1Key) { // get cache1 keys from cache2 Set<Cache1Key> keys = cache2Helper.get(cache2Key); if (keys != null) { for (Cache1Key key : keys) { // remove the cache1 cache1Helper.remove(key); // remove the link cache2Helper.remove(cache2Key, key); } } } }; @Around("@annotation(cachedMethod)") public Object around(ProceedingJoinPoint pjp, CachedMethod cachedMethod) throws Throwable { Object retVal = null; switch (cachedMethod.value()) { case GET: retVal = this.get(pjp, cachedMethod); break; case INSERT: case UPDATE: case DELETE: case INVALIDATE: retVal = this.invalidate(pjp); break; default: retVal = pjp.proceed(); break; } return retVal; } /** * Handles the {@link CacheStrategy#GET} strategy by intercepting a method * call with a cache lookup and caching it if it was not found. * * @param pjp * @return cached value or retVal * @throws Throwable */ @SuppressWarnings("unchecked") protected Object get(ProceedingJoinPoint pjp, CachedMethod annotation) throws Throwable { Object retVal = null; // the declaring class final Class<?> declaringClass = pjp.getTarget().getClass(); final Method method = AopUtils.getMostSpecificMethod( ((MethodSignature) pjp.getSignature()).getMethod(), declaringClass); // cache 1 key final Cache1Key cache1Key = CacheUtil.createCache1Key(declaringClass, method.getName(), method.getParameterTypes(), pjp.getArgs()); // return type of method final Class<?> returnType = method.getReturnType(); // if the return type is a list if (List.class.isAssignableFrom(returnType) && annotation.clazz().isAnnotationPresent(Cache2Element.class)) { CachedValue<List<Identifiable>> cachedValue = (CachedValue<List<Identifiable>>) cache1Helper .get(cache1Key); if (cachedValue != null) { retVal = cachedValue.getValue(); } else { // proceed retVal = pjp.proceed(); // create new cached value cachedValue = new CachedValue<List<Identifiable>>( (List<Identifiable>) retVal); // cache the value cache1Helper.put(cache1Key, cachedValue); // create links in cache2 this.handleFields(cachedValue.getValue(), cache1Key, this.putCommand); } } // if the return type is a normal cache2 element or the method is // annotated else if (returnType.isAnnotationPresent(Cache2Element.class) || method.isAnnotationPresent(Cache2Element.class)) { // check the cache CachedValue<Identifiable> cachedValue = (CachedValue<Identifiable>) cache1Helper .get(cache1Key); if (cachedValue != null) { retVal = cachedValue.getValue(); } else { // proceed retVal = pjp.proceed(); // create new cached value cachedValue = new CachedValue<Identifiable>( (Identifiable) retVal); // cache the value cache1Helper.put(cache1Key, cachedValue); // create links in cache2 this.handleFields(cachedValue.getValue(), cache1Key, this.putCommand); } } // if the return type is not an entity else { retVal = pjp.proceed(); } return retVal; } /** * Handles the {@link CacheStrategy#INVALIDATE} strategy by intercepting a * method call and invalidating any cached methods linked to method argument * elements. * * @param pjp * @return retVal * @throws Throwable */ protected Object invalidate(ProceedingJoinPoint pjp) throws Throwable { // the declaring class final Class<?> declaringClass = pjp.getTarget().getClass(); final Method method = AopUtils.getMostSpecificMethod( ((MethodSignature) pjp.getSignature()).getMethod(), declaringClass); final Object retVal = pjp.proceed(); // handle the arguments with the command this.handleArguments(pjp.getArgs(), method.getParameterAnnotations(), null, this.invalidateCommand); return retVal; } /** * Check each argument for the {@link Cache2Element} annotation and execute * the cache command for it. * * @param args * @param cache1Key * @param command * @throws Exception */ @SuppressWarnings("unchecked") private void handleArguments(Object[] args, Annotation[][] annotations, Cache1Key cache1Key, CacheCommand command) throws Exception { if (args != null) { for (int i = 0; i < args.length; i++) { Cache2Element cache2Element = null; final Object arg = args[i]; if (arg != null) { // first see if the argument is an annotated class cache2Element = arg.getClass().getAnnotation( Cache2Element.class); // if its not, check if it is an annotated argument if (cache2Element == null) { for (Annotation annotation : annotations[i]) { if (annotation instanceof Cache2Element) { cache2Element = (Cache2Element) annotation; break; } } } if (cache2Element == null) { continue; } // if its a list if (List.class.isAssignableFrom(arg.getClass())) { this.handleFields((List<Identifiable>) arg, cache1Key, command); } // if its an integer else if (int.class.isAssignableFrom(arg.getClass()) || Integer.class.isAssignableFrom(arg.getClass())) { command.execute(CacheUtil.createCache2Key( cache2Element.value(), (int) arg), cache1Key); } // if its a normal element else if (Identifiable.class .isAssignableFrom(arg.getClass())) { this.handleFields((Identifiable) arg, cache1Key, command); } } } } } /** * Delegates to {@link #handleFields(Identifiable, Cache1Key, CacheCommand)} * for each element in the list. * * @param elements * @param cache1Key * @param command * @throws Exception */ private void handleFields(List<Identifiable> elements, Cache1Key cache1Key, CacheCommand command) throws Exception { if (elements != null) { for (Identifiable element : elements) { this.handleFields(element, cache1Key, command); } } } /** * Recurses down the element's fields for {@link Cache2Element} annotations * and executes the command for each. * * @param element * @param cache1Key * @param command * @throws Exception */ @SuppressWarnings("unchecked") private void handleFields(Identifiable element, Cache1Key cache1Key, CacheCommand command) throws Exception { if (element != null && element.getClass().isAnnotationPresent(Cache2Element.class)) { // execute command command.execute( CacheUtil.createCache2Key(element.getClass(), element.getId()), cache1Key); // recurse for fields final Field[] fields = element.getClass().getDeclaredFields(); if (fields != null) { for (Field field : fields) { field.setAccessible(true); // if the field is annotated if (field.isAnnotationPresent(Cache2Element.class)) { // if its a list if (List.class.isAssignableFrom(field.getType())) { this.handleFields( (List<Identifiable>) field.get(element), cache1Key, command); } // if its an integer else if (int.class.isAssignableFrom(field.getType()) || Integer.class.isAssignableFrom(field .getType())) { // execute the command command.execute( CacheUtil.createCache2Key(field .getAnnotation(Cache2Element.class) .value(), (int) field.get(element)), cache1Key); } // if its a normal element else if (Identifiable.class.isAssignableFrom(field .getType())) { this.handleFields((Identifiable) field.get(element), cache1Key, command); } } } } } } }